package com.fc.mall.service.impl;

import com.fc.mall.config.SecurityHolder;
import com.fc.mall.dto.WXPayTransferResult;
import com.fc.mall.dto.WxTransferRequest;
import com.fc.mall.exception.InternalServerErrorException;
import com.fc.mall.exception.ServiceException;
import com.fc.mall.service.WxService;
import com.fc.mall.sysconfig.dao.SysConfigDao;
import com.fc.mall.sysconfig.dto.SysConfigDto;
import com.fc.mall.util.Utils;
import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayConfig;
import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayUtil;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.io.ByteStreams;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.fc.mall.util.Constants.SysPropertyConfigName.APPID;
import static com.fc.mall.util.Constants.SysPropertyConfigName.CERT;
import static com.fc.mall.util.Constants.SysPropertyConfigName.MCHID;
import static com.fc.mall.util.Constants.SysPropertyConfigName.PAYKEY;

@Slf4j
@Service
public class WxServiceImpl implements WxService {

    private static final String WX_TRANSFER_URL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers";

    private static final String DEFAULT_IP = "192.168.0.1";

    private static final int MAX_TRANSFER_RETRY = 3;

    private static final Integer WX_CONFIG_CATEGORY = 3;

    private static final String WX_CONFIG_CACHE_KEY = "wxConfigCacheKey";

    @Autowired
    private SysConfigDao sysConfigDao;

    private LoadingCache<String, WxConfig> wxConfigCache;

    @PostConstruct
    public void init() {
        wxConfigCache = CacheBuilder.newBuilder()
                .maximumSize(1).expireAfterWrite(300, TimeUnit.SECONDS)
                .build(new CacheLoader<String, WxConfig>() {
                    @Override
                    public WxConfig load(String key) throws IOException {
                        List<SysConfigDto> list = sysConfigDao.queryListByCategory(WX_CONFIG_CATEGORY);
                        Map<String, String> map = list.stream().collect(Collectors.toMap(SysConfigDto::getCode, SysConfigDto::getValue));
                        InputStream cert = new URL(map.get(CERT.getName())).openStream();
                        return WxConfig.builder()
                                .mchAppID(map.get(APPID.getName()))
                                .key(map.get(PAYKEY.getName()))
                                .certData(ByteStreams.toByteArray(cert))
                                .mchID(map.get(MCHID.getName())).build();
                    }
                });
    }

    @Override
    public WXPayTransferResult transfer(String openId, int amount, String description) {
        String tradeNo = Utils.generateId();
        int tries = 0;
        try {
            while (true) {
                WxConfig wxConfig = wxConfigCache.get(WX_CONFIG_CACHE_KEY);
                WxTransferRequest transferRequest = WxTransferRequest.builder()
                        .openId(openId)
                        .tradeNo(tradeNo)
                        .checkName(WxTransferRequest.CheckName.NO_CHECK)
                        .amount(amount)
                        .desc(description)
                        .ip(SecurityHolder.IP.get() == null ? DEFAULT_IP : SecurityHolder.IP.get())
                        .build();
                WXPay wxPay = getWxPay(wxConfig);
                String respXml = wxPay.requestWithCert(WX_TRANSFER_URL,
                        fillTransferRequestData(transferRequest, wxConfig),
                        wxConfig.getHttpConnectTimeoutMs(),
                        wxConfig.getHttpReadTimeoutMs());
                Map<String, String> response = wxPay.processResponseXml(respXml);
                WXPayTransferResult result = new WXPayTransferResult(response);

                if (WXPayTransferResult.ResultCode.SUCCESS == result.getResultCode() &&
                        WXPayTransferResult.ReturnCode.SUCCESS == result.getReturnCode()) {
                    return result;
                } else {
                    if ("SYSTEMERROR".equals(result.getErrCode())) {
                        if (tries++ < MAX_TRANSFER_RETRY) {
                            Thread.sleep(1000);
                            continue;
                        }
                        throw new ServiceException("微信支付系统繁忙");
                    }
                    String errMsg = result.getErrCodeDes() != null ? result.getErrCodeDes() : result.getReturnMsg();
                    throw new ServiceException(errMsg);
                }
            }
        } catch (ServiceException se) {
            throw se;
        } catch (Exception e) {
            LOGGER.error("WxServiceImpl.transfer error", e);
            throw new InternalServerErrorException(e.getMessage());
        }
    }

    private Map<String, String> fillTransferRequestData(WxTransferRequest transferRequest, WxConfig wxConfig)
            throws Exception {
        //一定要按字典顺序传参
        Map<String, String> data = new LinkedHashMap<>();

        this.putIfAvailable(data, "amount", String.valueOf(transferRequest.getAmount()));
        this.putIfAvailable(data, "check_name", transferRequest.getCheckName().name());
        this.putIfAvailable(data, "desc", transferRequest.getDesc());
        this.putIfAvailable(data, "mch_appid", wxConfig.getMchAppID());
        this.putIfAvailable(data, "mchid", wxConfig.getMchID());
        this.putIfAvailable(data, "nonce_str", WXPayUtil.generateNonceStr());
        this.putIfAvailable(data, "openid", transferRequest.getOpenId());
        this.putIfAvailable(data, "partner_trade_no", transferRequest.getTradeNo());
        this.putIfAvailable(data, "re_user_name", transferRequest.getReceiveUsername());
        this.putIfAvailable(data, "spbill_create_ip", transferRequest.getIp());

        data.put("sign", WXPayUtil.generateSignature(data, wxConfig.getKey(), WXPayConstants.SignType.MD5));
        return data;
    }

    private void putIfAvailable(Map<String, String> data, String key, String value) {
        if (!StringUtils.isEmpty(value)) {
            data.put(key, value);
        }
    }

    private WXPay getWxPay(WxConfig wxConfig) {
        return new WXPay(wxConfig);
    }

    @Getter
    @Setter
    @Builder
    static class WxConfig implements WXPayConfig {
        private String mchAppID;
        private String appID;
        private String key;
        private String mchID;

        @Getter(AccessLevel.NONE)
        private byte[] certData;

        private int httpConnectTimeoutMs = 8000;

        private int httpReadTimeoutMs = 10000;

        @Override
        public InputStream getCertStream() {
            return new ByteArrayInputStream(certData);
        }
    }
}
